/**
 * Copyright 2004 Cordys R&D B.V. 
 * 
 * This file is part of the Cordys FTP Connector. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 package com.eibus.applicationconnector.ftp;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;

import com.eibus.util.logger.CordysLogger;
import com.eibus.util.logger.Severity;
import com.eibus.xml.nom.Document;
import com.eibus.xml.nom.Node;

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.io.CopyStreamException;
import org.apache.commons.net.ftp.FTPConnectionClosedException;

/**
 * This class implements IFTPClient by abstracting
 * connection to Apache's FTP Client. Implements
 * the common commands of FTP.  
 */
public class CordysFTPClient implements IFTPClient
{
	private static Document document = null;
	public FTPClient ftpClient;
	private static final CordysLogger logger = CordysLogger.getCordysLogger(CordysFTPClient.class);
	private static final String FILE_NOT_FOUND = "The system cannot find the file specified";
	
	private static final String CREATE_DIRECTORY_FAILURE = "<dir_name>: Cannot create a file when that file already exists or the access is denied.";
	private static final String REMOVE_DIRECTORY_FAILURE = "<dir_name>: Cannot find the directory or the directory is not empty.";
	
	/**
	 * Connects to the ftp server whose connection details
	 * are available in ftpConfiguration
	 */
	@Override
	public void connect(FTPConfiguration ftpConfiguration) throws SocketException, IOException, FTPException
	{
		document = ftpConfiguration.getDocument();
		
		ftpClient = new FTPClient();
		
		/* Do the actual connection here */
		ftpClient.connect(ftpConfiguration.getServer(), ftpConfiguration.getPort());
		
		/* Login to the server with the given user credentials */
		ftpClient.login(ftpConfiguration.getUsername(), ftpConfiguration.getPassword());
		
		/* 
		 * Check the mode in the which connection has to be made - ACTIVE or PASSIVE
		 * and switch the ftpClient accordingly
		 */
		if (ftpConfiguration.isActiveMode())
		{
			logger.log(Severity.INFO, "Swtiching to ACTIVE mode.");
			ftpClient.enterLocalActiveMode();
		}
		else
		{
			logger.log(Severity.INFO, "Swtiching to PASSIVE mode.");
			ftpClient.enterLocalPassiveMode();
		}
		
		/* Any error while CONNECT or LOGIN, throw an exception */ 
		if(getReplyString().contains("530") || getReplyString().contains("500") || getReplyString().contains("501"))
        {
			logger.log(Severity.ERROR, "An error has occurred while CONNECT/LOGIN to FTP server. Response got from ftpclient is " + getReplyString());
			throw (new FTPException(FTPHandler.serverFaultCode, getReplyString()));
 		}
	}

	/**
	 * APPEND command implementation
	 * sourceFile: file on the ftp client
	 * targetFile: file on the ftp server
	 */
	@Override
	public String append(String sourceFile, String targetFile) throws FTPException
	{
		BufferedInputStream in;
		try
		{
			/* create an input stream and pass it to appendFile() api */
			in = new BufferedInputStream(new FileInputStream(sourceFile));
			ftpClient.appendFile(targetFile, in);
			in.close();
			
			/* Any error while APPENDing, throw the exception */
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing APPEND command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString().substring(4)));
			}
		}
		catch (FileNotFoundException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing APPEND command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing APPEND command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();
	}
	
	/**
	 * MKDIR command implementation
	 * path: Path of the directory to be created, relative to FTP's root directory 
	 */
	@Override
	public String createDirectory(String path) throws FTPException
	{
		try 
		{
			ftpClient.makeDirectory(path);
			
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing MKDIR command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, CREATE_DIRECTORY_FAILURE.replace("<dir_name>", path)));
			}
			
		} 
		catch (IOException e) 
		{
			logger.log(Severity.ERROR, "An error has occurred while executing MKDIR command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();	
	}
	
	/**
	 * DELETE command implementation
	 * path: path of the file to be deleted
	 */
	@Override
	public String delete(String path) throws FTPException
	{
		try
		{
			ftpClient.deleteFile(path);
			
			if(getReplyString().contains("550"))
	        {
				logger.log(Severity.ERROR, "An error has occurred while executing DELETE command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString()));
	 		}
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DELETE command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();
	}

	/**
	 * Multiple files delete implementation
	 * If path refers to a file, delete it
	 * If path refers to a directory, delete all the files in that directory
	 */
	@Override
	public int deleteFiles(String path) throws FTPException
	{
		int nodeToReturn = document.createElement("nodeToReturn");
		
		if (isDirectory(path))
		{
			logger.log(Severity.INFO, "Given path refers to a directory.");
			Node.appendToChildren(deleteFilesInDirectory(path), nodeToReturn);
		}
		else
		{
			logger.log(Severity.INFO, "Given path refers to a file.");
			String replyString = delete(path);
			document.createTextElement("statuscode", replyString.substring(0, 3), nodeToReturn);
			document.createTextElement("status", replyString.substring(4), nodeToReturn);
		}
		return nodeToReturn;
	}
	
	/**
	 * Delete all the files in the given directory
	 * @param path
	 * @return
	 * @throws FTPException
	 */
	private int deleteFilesInDirectory(String path)  throws FTPException
	{
		Vector<String> fileList = new Vector<String>();
		/* Get the list of files inside 'path' */
		getListOfFiles(path, fileList);
		
		/* 
		 * fileList contains files with absolute path.
		 * Get only the filenames from that
		 */ 
		
		int filesNode = 0;

		filesNode = document.createElementNS("dir", null, null, FTPCommand.METHOD_NAMESPACE, 0);
		
		/* For all the files in fileList, call the delete() method to delete the file */
		for (String filePath : fileList)
		{
			filePath = filePath.substring(filePath.lastIndexOf("/") + 1);
			int fileNode = document.createElementNS("file", filePath, null, FTPCommand.METHOD_NAMESPACE, filesNode);
			String replyString = delete(path + '/' + filePath);
			document.createElementNS("statuscode", replyString.substring(0, 3), null, FTPCommand.METHOD_NAMESPACE, fileNode);
			document.createElementNS("status", replyString.substring(4), null, FTPCommand.METHOD_NAMESPACE, fileNode);
		}
		
		return filesNode;
	}

	/**
	 * isDirectory() returns true if the path passed to it represents a directory
	 * else, it returns false
	 * @param path
	 * @return
	 */
	private boolean isDirectory(String path) throws FTPException
	{
		String parentDirectory = path.substring(0, path.lastIndexOf('/')+1);
		TreeMap<String, FTPFile> fileListInParentDirectory = new TreeMap<String, FTPFile>();
		getListOfFilesWithNames(parentDirectory, fileListInParentDirectory);
		
		Set entrySet = fileListInParentDirectory.entrySet();
		Iterator entrySetIter = entrySet.iterator();
		String fileName = "";
		FTPFile ftpFile = null;
		
		while(entrySetIter.hasNext())
		{
			Map.Entry entry = (Map.Entry) entrySetIter.next();
			fileName = (String)entry.getKey();
			ftpFile = (FTPFile)entry.getValue();
			
			if (path.equals(fileName) && ftpFile.isDirectory())
			{
				return true;
			}
		}
		
		return false;
	}

	/**
	 * GET command implementation
	 */
	@Override
	public String download(String sourceFile, String targetFile) throws FTPException
	{
		FileOutputStream fos;
		File newFile = new File(targetFile);
		logger.log(Severity.INFO, "Executing DOWNLOAD command. Response got from ftpclient is " + getReplyString());
		
		try
		{
			fos = new FileOutputStream (newFile);
			ftpClient.retrieveFile(sourceFile,fos);
			fos.close();
			
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing DOWNLOAD command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString()));
			}
		}
		catch (FileNotFoundException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DOWNLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		catch (FTPConnectionClosedException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DOWNLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		catch (CopyStreamException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DOWNLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DOWNLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		catch (Exception e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DOWNLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		finally
		{
			/* If the source file doesn't exists on ftpserver, delete the newly created file on the client */
			if (getReplyString().contains(FILE_NOT_FOUND))
			{
				newFile.delete();
			}
		}

		return getReplyString();
	}

	/**
	 * LS command implementation
	 */
	@Override
	public String getListOfFiles(String path, List<String> fileList) throws FTPException
	{
		try
		{
			String[] ftpFileList;
			
			/* If there is no path given, get from the ftp root directory */
			if (path.equals(""))
			{
				ftpFileList = ftpClient.listNames();
			}
			else
			{
				ftpFileList = ftpClient.listNames(path);				
			}
			
			if (getReplyString().contains(FILE_NOT_FOUND))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing getListOfFiles method. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString()));
			}

			if (ftpFileList != null)
			{
				for (String fileName : ftpFileList)
				{
					fileName = fileName.replace('\\', '/');
					fileList.add(fileName);
				}
			}
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing getListOfFiles command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();
	}

	@Override
	public String getListOfFilesWithDetails(String path, List<CordysFTPFile> cordysFTPFileList) throws FTPException 
	{
		try
		{
			FTPFile[] ftpFileList;
			
			/* If there is no path given, get from the ftp root directory */
			if (path.equals(""))
			{
				ftpFileList = ftpClient.listFiles();
			}
			else
			{
				ftpFileList = ftpClient.listFiles(path);				
			}
			
			if (getReplyString().contains(FILE_NOT_FOUND))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing getListOfFiles method. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString(), path));
			}

			if (ftpFileList != null)
			{
				for (FTPFile ftpFile : ftpFileList)
				{
					CordysFTPFile cordysFTPFile = new CordysFTPFile();
					cordysFTPFile.setFileName(ftpFile.getName().replace('\\', '/'));
					cordysFTPFile.setDirectory(ftpFile.isDirectory());
					cordysFTPFile.setFileSize(ftpFile.getSize());
					Date date = ftpFile.getTimestamp().getTime();
					cordysFTPFile.setFileModificationDate(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date));
					cordysFTPFileList.add(cordysFTPFile);
				}
			}
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing getListOfFiles command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();
	}
	
	/**
	 * DIR command implementation
	 */
	@Override
	public int listDirectories(int pathNode) throws FTPException
	{
		/* get the value of 'recursive' attribute */
		String recurssiveAttrVal = Node.getAttribute(pathNode, "recursive", "false");
		String dir = Node.getDataWithDefault(pathNode, "");
		
		dir = FTPCommand.getProperFileSeparator(dir, true);
		
		/* If recursive is set to true, display the directory listing recursively */
		if(recurssiveAttrVal.equals("true"))
		{
			int	fileSetNode = document.createElementNS("listOfFiles", null, null, FTPCommand.METHOD_NAMESPACE, 0);
			return listDirectoryRecursively(dir, fileSetNode);
		}
		else
		{
			int	listNode = document.createElementNS("listOfFiles", null, null, FTPCommand.METHOD_NAMESPACE, 0);
			
			Vector<String> listOfFiles = new Vector<String>();
			String replyString = getListOfFiles(dir, listOfFiles);
			
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing listDirectory command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString()));
			}
			
	        for(String filename : listOfFiles)
	        {
				document.createTextElement("file", filename, listNode);
			}
	        
	        Node.appendToChildren(document.createTextElement("statuscode", replyString.substring(0, 3)), listNode);
			Node.appendToChildren(document.createTextElement("status", replyString.substring(4)), listNode);
			return listNode;
		}
	}
	
	/**
	 * @param directoryPath
	 * @param fileSetNode
	 * @return
	 */
	private int listDirectoryRecursively(String directoryPath, int fileSetNode) throws FTPException
	{
		TreeMap<String, FTPFile> ftpFiles = new TreeMap<String, FTPFile>();
		String replyString = getListOfFilesWithNames(directoryPath, ftpFiles);
		
		int dirNode = document.createElementNS("dir", null, null, FTPCommand.METHOD_NAMESPACE, fileSetNode);
		int listNode = document.createElementNS("listOfFiles", null, null, FTPCommand.METHOD_NAMESPACE, dirNode);
		Node.setAttribute(dirNode, "path", directoryPath);
		
		Set entrySet = ftpFiles.entrySet();
		Iterator entrySetIter = entrySet.iterator();
		String fileName = "";
		FTPFile ftpFile = null;
		
		while(entrySetIter.hasNext())
		{
			Map.Entry entry = (Map.Entry) entrySetIter.next();
			ftpFile = (FTPFile)entry.getValue();
			
			Node.appendToChildren(document.createTextElement("file", ftpFile.getName()), listNode);
			if (ftpFile.isDirectory())
			{
				String dirPath=ftpFile.getName();
				if (!directoryPath.equals(""))
				{
					dirPath = directoryPath + "/" + ftpFile.getName();
				}
				listDirectoryRecursively(dirPath, fileSetNode);
			}
		}
		
		Node.appendToChildren(document.createTextElement("statuscode", replyString.substring(0, 3)), listNode);
		Node.appendToChildren(document.createTextElement("status", replyString.substring(4)), listNode);
		return fileSetNode;
	}
	
	/**
	 * Get the list of files (FTPFile objects) along with their names
	 * @param path
	 * @param filesList
	 * @return
	 */
	private String getListOfFilesWithNames(String path, Map<String, FTPFile> filesList) throws FTPException
	{
		try
		{
			String[] ftpFileList = ftpClient.listNames(path);
			FTPFile[] ftpFiles = ftpClient.listFiles(path);
			
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing listDirectory command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString()));
			}
			
			if (ftpFileList == null)
			{
				return "";
			}
			
			for(int i = 0; i < ftpFileList.length; i ++)
			{
				filesList.put(ftpFileList[i], ftpFiles[i]);
			}
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "@IOException: " + e.getMessage(), e);
		}
		
		return getReplyString();
	}

	/**
	 * PWD command implementation
	 */
	@Override
	public String printWorkingDirectory()
	{
		try 
		{
			ftpClient.printWorkingDirectory();
		} 
		catch (IOException e) 
		{
			logger.log(Severity.ERROR, "An error has occurred while executing PWD command. " + e.getMessage());
		}
		return getReplyString();
	}

	/**
	 * RMDIR command implementation
	 */
	@Override
	public String removeDirectory(String directoryPath) throws FTPException
	{
		try 
		{
			ftpClient.removeDirectory(directoryPath);
			
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing RMDIR command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, REMOVE_DIRECTORY_FAILURE.replace("<dir_name>", directoryPath)));
			}
		} 
		catch (IOException e) 
		{
			logger.log(Severity.ERROR, "An error has occurred while executing PWD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();
	}

	/**
	 * RENAME command implementation
	 */
	@Override
	public String rename(String oldFileName, String newFileName) throws FTPException
	{
		try
		{
			ftpClient.rename(oldFileName, newFileName);
			
			if (getReplyString().contains("550"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing RENAME command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString().substring(4)));
			}
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing RENAME command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return getReplyString();
	}

	/**
	 * PUT command implementation
	 */
	@Override
	public String upload(String sourceFile, String targetFile) throws FTPException
	{
		try
		{
			BufferedInputStream in = new BufferedInputStream(new FileInputStream(sourceFile));
			ftpClient.storeFile(targetFile, in);
			in.close();
			
			if (getReplyString().contains("550") || getReplyString().contains("553"))
			{
				logger.log(Severity.ERROR, "An error has occurred while executing UPLOAD command. Response got from ftpclient is " + getReplyString());
				throw (new FTPException(FTPHandler.serverFaultCode, getReplyString().substring(4)));
			}
		}
		catch (FileNotFoundException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing UPLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing UPLOAD command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}

		return getReplyString();
	}
	
	/**
	 * Disconnect from the server and logout
	 */
	@Override
	public void disconnect()
	{
		try
		{
			/* Make sure the client is disconnected only if it is already connected */
			if (ftpClient.isConnected())
			{
				ftpClient.logout();
				ftpClient.disconnect();		
				logger.log(Severity.DEBUG, "Logging out from FTP Server - " + getReplyString());
			}
		}
		catch (IOException e)
		{
			logger.log(Severity.ERROR, "An error occurred while logging out of FTP Server. " + e.getMessage(), e);
		}
	}

	/**
	 * Set the transfer type. Supported types are - ASCII and BINARY 
	 */
	@Override
	public void setFileType(FILE_TYPE fileType)
	{
		try
		{
			switch (fileType)
			{
				case ASCII:
							ftpClient.setFileType(FTP.ASCII_FILE_TYPE);
							break;
				case BINARY:
							ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
							break;
				default:
							break;
			}
		}
		catch (Exception e)
		{
			logger.log(Severity.ERROR, "An error during setting file type to " + fileType.toString(), e);
		}
	}
	
	/**
	 * Get the status message from the ftpClient.
	 * @return
	 */
	public String getReplyString()
	{
		return (ftpClient.getReplyString());
	}
}